package mergeSort;

import lock.ConcreteLock;
import lock.Lock;
import shared.Buffer;
import shared.MyComparable;
import shared.MyStop;
import shared.SortInput;
import shared.SortInputVisitor;

public class MergeSorter<T extends Comparable<? super T>> implements Runnable{

	private boolean started;
	private final Lock resultLock = new ConcreteLock(true);
	private boolean done;
	private final Buffer<SortInput<T>> firstSplitBuffer;
	private final Buffer<SortInput<T>> secondSplitBuffer;
	private final Buffer<SortInput<T>> inputBuffer;
	private final Buffer<T> outputBuffer;
	private boolean putInFirst = true;

	public boolean isStarted() {
		return started;
	}

	public void setStarted(final boolean started) {
		this.started = started;
	}

	private Lock getResultLock() {
		return resultLock;
	}

	private boolean isDone() {
		return done;
	}

	private void setDone() {
		this.done = true;
		this.getResultLock().unlock();
	}
	
	private Buffer<SortInput<T>> getFirstSplitBuffer() {
		return firstSplitBuffer;
	}	
	

	private Buffer<SortInput<T>> getSecondSplitBuffer() {
		return secondSplitBuffer;
	}
	
	
	private Buffer<SortInput<T>> getInputBuffer() {
		return inputBuffer;
	}


	public Buffer<T> getOutputBuffer() {
		return outputBuffer;
	}
	
	/**
	 * Constructor of {@link MergeSorter}
	 * @param inputBuffer is the input buffer with a {@link MyStop} at the end.
	 */
	public MergeSorter(final Buffer<SortInput<T>> inputBuffer) {
		this.inputBuffer = inputBuffer;
		this.firstSplitBuffer = new Buffer<SortInput<T>>();
		this.secondSplitBuffer = new Buffer<SortInput<T>>();
		this.outputBuffer = new Buffer<T>();
	}
	
	@Override
	public void run() {
		while(!this.isDone()){
			final SortInput<T> input = this.getInputBuffer().get();
			input.accept(new SortInputVisitor<T>() {

				@Override
				public void visit(final MyComparable<T> myComparable) {
					MergeSorter.this.split(myComparable);
				}

				@Override
				public void visit(final MyStop<T> myStop) {
					final boolean firstSplitBufferNotEmpty = !MergeSorter.this.getFirstSplitBuffer().isEmpty();
					final boolean secondSplitBufferNotEmpty = !MergeSorter.this.getSecondSplitBuffer().isEmpty();
					
					if(firstSplitBufferNotEmpty && secondSplitBufferNotEmpty){
						MergeSorter.this.getFirstSplitBuffer().put(myStop);
						MergeSorter.this.getSecondSplitBuffer().put(myStop);
						final MergeSorter<T> firstMergeSorter = new MergeSorter<T>(MergeSorter.this.getFirstSplitBuffer());
						final MergeSorter<T> secondMergeSorter = new MergeSorter<T>(MergeSorter.this.getSecondSplitBuffer());
						firstMergeSorter.start();
						secondMergeSorter.start();
						final Buffer<T> firstResult = firstMergeSorter.getResult();
						final Buffer<T> secondResult = secondMergeSorter.getResult();
						MergeSorter.this.mergeResults(firstResult, secondResult);
					} else {
						if(firstSplitBufferNotEmpty){
							final SortInput<T> onlyInput = MergeSorter.this.getFirstSplitBuffer().get();
							onlyInput.accept(new SortInputVisitor<T>() {

								@Override
								public void visit(final MyComparable<T> myComparable) {
									MergeSorter.this.getOutputBuffer().put(myComparable.getComparable());
								}

								@Override
								public void visit(final MyStop<T> myStop) {
								}
							});
							
						}
					}
					MergeSorter.this.setDone();
				}
			});
		}
	}

	/**
	 * Merges the provided results in the outputBuffer of this MergeSorter.
	 * @param firstResult is the first result.
	 * @param secondResult is the second result.
	 */
	protected void mergeResults(final Buffer<T> firstResult, final Buffer<T> secondResult) {
		while(!firstResult.isEmpty() || !secondResult.isEmpty()){
			if(!firstResult.isEmpty() && !secondResult.isEmpty()){
				if(firstResult.peek().compareTo(secondResult.peek()) < 0){
					this.getOutputBuffer().put(firstResult.get());
				} else {
					this.getOutputBuffer().put(secondResult.get());
				}
			} else{
				if(!firstResult.isEmpty()){
					this.getOutputBuffer().put(firstResult.get());
				} else {
					if(!secondResult.isEmpty()){
						this.getOutputBuffer().put(secondResult.get());
					} 
				}
			}
		}
	}

	/**
	 * Puts the provided comparable in either the firstSplitBuffer or secondSplitBuffer alternatively.
	 * @param myComparable is the comparable to put in one of the two split buffers.
	 */
	protected void split(final MyComparable<T> myComparable) {
		if(this.putInFirst){
			this.getFirstSplitBuffer().put(myComparable);
		} else {
			this.getSecondSplitBuffer().put(myComparable);
		}
		this.putInFirst = !this.putInFirst;
	}
	
	/**
	 * Starts this sorter when its is not started yet.
	 */
	private void start(){
		if(!this.isStarted()){
			new Thread(this).start();
			this.setStarted(true);
		}
	}

	/**
	 * Provides the result of this MergeSorter.
	 * Locks until the result of this mergeSorter is done.
	 * @return is the result of this MergeSorter.
	 */
	public Buffer<T> getResult() {
		this.start();
		this.getResultLock().lock();
		final Buffer<T> result = this.getOutputBuffer();
		this.getResultLock().unlock();
		return result;
	}
}
